package org.kroz.activerecord;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.kroz.activerecord.utils.ARConst;
import org.kroz.activerecord.utils.Logg;

import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;

/**
 * Represents a database to be used by Android Active Record entities.
 * 
 * @author JEREMYOT
 * @author Vladimir Kroz
 * 
 *         <p>
 *         This project based on and inspired by 'androidactiverecord' project written by JEREMYOT
 *         </p>
 */
public class Database {
  
  static final String CNAME = Database.class.getSimpleName();
  
  static Map<String, DatabaseBuilder> _builders = new HashMap<String, DatabaseBuilder>();
  
  private SQLiteDatabase _database;
  
  private DatabaseOpenHelper _dbHelper;
  
  private String _path;
  
  @SuppressWarnings("unused")
  private Context _context;
  
  /**
   * Creates a new DatabaseWrapper object
   * 
   * @param dbName
   *          The file name to use for the SQLite database.
   * @param dbVersion
   *          Database version
   * @param context
   *          The context used for database creation, its package name will be used to place the database on external storage if any is present, otherwise the context's application data directory.
   */
  Database(Context context, String dbName, int dbVersion, DatabaseBuilder builder) {
    _context = context;
    // String dbPath = (Environment.getExternalStorageState().equals(
    // Environment.MEDIA_MOUNTED) ? appendFilePath(Environment
    // .getExternalStorageDirectory().getAbsolutePath(), String
    // .format("android%1$sdata%1$s%2$s%1$s", File.separator, _context
    // .getPackageName())) : _context.getDir(
    // _context.getPackageName(), 0).getAbsolutePath());
    // new File(dbPath).mkdirs();
    _path = dbName; // temporary workaround - DB is created only on device,
    // not SDcard
    // _path = appendFilePath(dbPath, dbName);
    _dbHelper = new DatabaseOpenHelper(context, _path, dbVersion, builder);
    _context = context;
  }
  
  /**
   * Creates new DB instance. Returned DB instances is not initially opened. Calling application must explicitly open it by calling open() method
   * 
   * @param ctx
   * @param dbName
   * @param dbVersion
   * @return
   * @throws ActiveRecordException
   */
  public static Database createInstance(Context ctx, String dbName, int dbVersion) throws ActiveRecordException {
    DatabaseBuilder builder = getBuilder(dbName);
    if (null == builder)
      throw new ActiveRecordException("Schema wasn't initialized. Call Database.setBuilder() first");
    return new Database(ctx, dbName, dbVersion, builder);
  }
  
  /**
   * Creates and opens new DB instances. DB instance returned to calling application already opened.
   * 
   * @param ctx
   * @param dbName
   * @param dbVersion
   * @return
   * @throws ActiveRecordException
   */
  public static Database open(Context ctx, String dbName, int dbVersion) throws ActiveRecordException {
    Database db = Database.createInstance(ctx, dbName, dbVersion);
    db.open();
    return db;
  }
  
  /**
   * Returns DatabaseBuilder object assosicted with Database
   * 
   * @param dbName
   *          database name
   * @return DatabaseBuilder object assosicted with Database
   */
  public static DatabaseBuilder getBuilder(String dbName) {
    return _builders.get(dbName);
  }
  
  /**
   * Initializes Database framework. This method must be called for each used database only once before using database. This is required for proper setup static attributes of the Database
   * 
   * @param builder
   * @return
   */
  static public void setBuilder(DatabaseBuilder builder) {
    _builders.put(builder.getDatabaseName(), builder);
  }
  
  public static Database createInstance(Context ctx, String dbName, int dbVersion, DatabaseBuilder builder) {
    return new Database(ctx, dbName, dbVersion, builder);
  }
  
  /**
   * Opens or creates the database file. Uses external storage if present, otherwise uses local storage.
   */
  public void open() {
    if (_database != null && _database.isOpen()) {
      _database.close();
      _database = null;
    }
    _database = _dbHelper.getReadableDatabase();
    Logg.d(ARConst.TAG, "(%t) %s.open(): new db obj %s", CNAME, _database.toString());
  }
  
  public void close() {
    String d = _database.toString();
    if (_database != null) _database.close();
    _database = null;
    Logg.d(ARConst.TAG, "(%t) %s.close(): db obj %s set to null", CNAME, d);
  }
  
  public boolean isOpen() {
    return null != _database && _database.isOpen();
  }
  
  /**
   * Execute some raw SQL.
   * 
   * @param sql
   *          Standard SQLite compatible SQL.
   */
  public void execute(String sql) {
    _database.execSQL(sql);
  }
  
  /**
   * Insert into a table in the database.
   * 
   * @param table
   *          The table to insert into.
   * @param parameters
   *          The data.
   * @return the row ID of the newly inserted row, or -1 if an error occurred.
   */
  public long insert(String table, ContentValues parameters) {
    return _database.insert(table, null, parameters);
  }
  
  /**
   * Update a table in the database.
   * 
   * @param table
   *          The table to update.
   * @param values
   *          The new values.
   * @param whereClause
   *          The condition to match (Don't include "where").
   * @param whereArgs
   *          The arguments to replace "?" with.
   * @return The number of rows affected.
   */
  public int update(String table, ContentValues values, String whereClause, String[] whereArgs) {
    return _database.update(table, values, whereClause, whereArgs);
  }
  
  /**
   * Delete from a table in the database
   * 
   * @param table
   *          The table to delete from.
   * @param whereClause
   *          The condition to match (Don't include WHERE).
   * @param whereArgs
   *          The arguments to replace "?" with.
   * @return The number of rows affected.
   */
  public int delete(String table, String whereClause, String... whereArgs) {
    return _database.delete(table, whereClause, whereArgs);
  }
  
  /**
   * Execute a raw SQL query.
   * 
   * @param sql
   *          Standard SQLite compatible SQL.
   * @return A cursor over the data returned.
   */
  public Cursor rawQuery(String sql) {
    return rawQuery(sql, (String[]) null);
  }
  
  /**
   * Execute a raw SQL query.
   * 
   * @param sql
   *          Standard SQLite compatible SQL.
   * @param params
   *          The values to replace "?" with.
   * @return A cursor over the data returned.
   */
  public Cursor rawQuery(String sql, String... params) {
    return _database.rawQuery(sql, params);
  }
  
  /**
   * Execute a query.
   * 
   * @param table
   *          The table to query.
   * @param selectColumns
   *          The columns to select.
   * @param where
   *          The condition to match (Don't include "where").
   * @param whereArgs
   *          The arguments to replace "?" with.
   * @return A cursor over the data returned.
   * @throws ActiveRecordException
   *           is database is null or closed
   */
  public Cursor query(String table, String[] selectColumns, String where, String... whereArgs)
      throws ActiveRecordException {
    return query(false, table, selectColumns, where, whereArgs, null, null, null, null);
  }
  
  /**
   * Execute a query.
   * 
   * @param distinct
   * @param table
   *          The table to query.
   * @param selectColumns
   *          The columns to select.
   * @param where
   *          The condition to match (Don't include "where").
   * @param whereArgs
   *          The arguments to replace "?" with.
   * @param groupBy
   * @param having
   * @param orderBy
   * @param limit
   * @return A cursor over the data returned.
   * @throws ActiveRecordException
   *           is database is null or closed
   */
  public Cursor query(boolean distinct, String table, String[] selectColumns, String where, String[] whereArgs,
      String groupBy, String having, String orderBy, String limit) throws ActiveRecordException {
    if (null == _database || !_database.isOpen()) {
      Logg.e(ARConst.TAG, "(%t) %s.query(): ERROR - db object is null or closed", CNAME);
      throw new ActiveRecordException(ErrMsg.ERR_DB_IS_NOT_OPEN);
    }
    
    return _database.query(distinct, table, selectColumns, where, whereArgs, groupBy, having, orderBy, limit);
  }
  
  /**
   * Returns array of database tables names
   * 
   * @throws ActiveRecordException
   */
  public String[] getTables() throws ActiveRecordException {
    if (null == _database || !_database.isOpen()) {
      Logg.e(ARConst.TAG, "(%t) %s.getTables(): ERROR - db object is null or closed", CNAME);
      throw new ActiveRecordException(ErrMsg.ERR_DB_IS_NOT_OPEN);
    }
    
    Cursor c = query("sqlite_master", new String[] { "name" }, "type = ?", new String[] { "table" });
    List<String> tables = new ArrayList<String>();
    try {
      while (c.moveToNext()) {
        tables.add(c.getString(0));
      }
    }
    finally {
      c.close();
    }
    return tables.toArray(new String[0]);
  }
  
  public String[] getColumnsForTable(String table) {
    Cursor c = rawQuery(String.format("PRAGMA table_info(%s)", table));
    List<String> columns = new ArrayList<String>();
    try {
      while (c.moveToNext()) {
        columns.add(c.getString(c.getColumnIndex("name")));
      }
    }
    finally {
      c.close();
    }
    return columns.toArray(new String[0]);
  }
  
  public int getVersion() throws ActiveRecordException {
    if (null == _database || !_database.isOpen()) {
      Logg.e(ARConst.TAG, "(%t) %s.getVersion(): ERROR - db object is null or closed", CNAME);
      throw new ActiveRecordException(ErrMsg.ERR_DB_IS_NOT_OPEN);
    }
    
    return _database.getVersion();
  }
  
  public void setVersion(int version) throws ActiveRecordException {
    if (null == _database || !_database.isOpen()) {
      Logg.e(ARConst.TAG, "(%t) %s.setVersion(): ERROR - db object is null or closed", CNAME);
      throw new ActiveRecordException(ErrMsg.ERR_DB_IS_NOT_OPEN);
    }
    
    _database.setVersion(version);
  }
  
  public void beginTransaction() {
    _database.beginTransaction();
  }
  
  public void endTransaction() {
    _database.endTransaction();
  }
  
  /**
   * Get the SQLite type for an input class.
   * 
   * @param c
   *          The class to convert.
   * @return A string representing the SQLite type that would be used to store that class.
   */
  protected static String getSQLiteTypeString(Class<?> c) {
    String name = c.getName();
    if (name.equals("java.lang.String")) return "text";
    if (name.equals("short")) return "int";
    if (name.equals("int")) return "int";
    if (name.equals("long")) return "int";
    if (name.equals("long")) return "int";
    if (name.equals("java.sql.Timestamp")) return "int";
    if (name.equals("double")) return "real";
    if (name.equals("float")) return "real";
    if (name.equals("[B")) return "blob";
    if (name.equals("boolean")) return "bool";
    if (c.getSuperclass() == ActiveRecordBase.class) return "int";
    throw new IllegalArgumentException("Class cannot be stored in Sqlite3 database.");
  }
  
  //	/**
  //	 * Append to a file path, takes extra or missing separator characters into
  //	 * account.
  //	 *
  //	 * @param path
  //	 *            The root path.
  //	 * @param append
  //	 *            What to add.
  //	 * @return The new path.
  //	 */
  //	private static String appendFilePath(String path, String append) {
  //		return path.concat(path.endsWith(File.separator) ? (append
  //				.startsWith(File.separator) ? append.substring(1) : append)
  //				: File.separator
  //						.concat((append.startsWith(File.separator) ? append
  //								.substring(1) : append)));
  //	}
  
  // -------------------------------------------------------------------------//
}
